package org.eclipse.iota.util;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.iota.IotaFactory;
import org.eclipse.iota.IotaPackage;
import org.eclipse.iota.model.BinaryOperation;
import org.eclipse.iota.model.Clause;
import org.eclipse.iota.model.Constant;
import org.eclipse.iota.model.Division;
import org.eclipse.iota.model.Element;
import org.eclipse.iota.model.EquationType;
import org.eclipse.iota.model.Exponentiation;
import org.eclipse.iota.model.Expression;
import org.eclipse.iota.model.MultiOperation;
import org.eclipse.iota.model.Reference;
import org.eclipse.iota.model.UnaryOperation;
import org.eclipse.iota.model.Variable;
import org.eclipse.iota.rewrite.IotaRule;
import org.eclipse.iota.rewrite.IotaRuleMatchFinder;

/**
 * Random generator for Iota model elements.
 * @author Christian Krause
 * @generated NOT
 */
public class IotaGenerator {
	
	// Upper bound for constants.
	private int upperBound = 10;
	
	// Lower bounds for constants.
	private int lowerBound = -10;
	
	// Expression types to be used.
	private List<EClass> expressionTypes;
	
	// Allowed equation types.
	private List<EquationType> equationTypes;
	
	// Rules for simplifying the generated models.
	private List<IotaRule> simplifyRules;
	
	/**
	 * Default constructor.
	 */
	public IotaGenerator() {
		expressionTypes = new ArrayList<EClass>();
		equationTypes = new ArrayList<EquationType>();
		simplifyRules = new ArrayList<IotaRule>();
	}
	
	/**
	 * Generate a random expression.
	 * @param depth Maximum depth of the expression.
	 * @param operands Maximum number of child operands.
	 * @return
	 */
	public Expression generateExpression(int depth, int operands, Clause clause) {
		
		// Nothing to do?
		if (depth<=0 || expressionTypes.isEmpty()) return null;
		List<Variable> variables = IotaVariableUtil.getVariables(clause);
				
		// Create a constant of variable?
		int types = expressionTypes.size();
		if (depth==1 || ((int) (Math.random()*(double) types)<2)) {
			
			if (Math.random()<0.5 && !variables.isEmpty()) {
				return new Reference(randomElem(variables));
			} else {
				int range = upperBound - lowerBound;
				int value = upperBound - (int) (Math.random() * (double) range);
				return new Constant(value);
			}
			
		}
		
		// Create a non-terminal expression.
		Expression expression = null;
		while (expression==null || expression instanceof Reference || expression instanceof Constant) {
			expression = (Expression) IotaFactory.eINSTANCE.create(randomElem(expressionTypes));
		}
		
		// Initialize children.
		if (expression instanceof UnaryOperation) {
			
			Expression operand = generateExpression(depth-1, operands, clause);
			((UnaryOperation) expression).setOperand(operand);
			
		}
		
		if (expression instanceof BinaryOperation) {
			
			BinaryOperation binary = (BinaryOperation) expression;
			Expression left = generateExpression(depth-1, operands, clause);
			Expression right = generateExpression(depth-1, operands, clause);
			
			// Test for some incompatibilities.
			if (expression instanceof Division && right instanceof Constant) {
				Constant divisor = (Constant) right;
				if (divisor.isZero()) {
					int value = 1 + (int) (Math.random()* (double) upperBound);
					right = new Constant(value);
				}
			}
			
			if (expression instanceof Exponentiation) {
				
				// Don't allow nested exponentiations.
				while (containsOrIsExponentiation(left))
					left = generateExpression(depth-1, operands, clause);
				
				while (containsOrIsExponentiation(right))
					right = generateExpression(depth-1, operands, clause);				
			}
			
			// Set left and right-hand side.
			binary.setLeft(left);
			binary.setRight(right);

		}
		
		else if (expression instanceof MultiOperation) {
			
			double stopProbability = 1.0 / (double) operands;
			operands = Math.max(operands, 2);
			
			for (int i=0; i<operands; i++) {
				if (i>1 && Math.random()<stopProbability) break;
				Expression child = generateExpression(depth-1, operands, clause);
				((MultiOperation) expression).getOperands().add(child);
			}
			
		}
		
		// Simplify the expression.
		simplify(expression);
		
		// Make sure that the constants are not too big.
		List<Constant> constants = IotaFindUtil.getAllExpressions(expression, 
													IotaPackage.eINSTANCE.getConstant());
		for (Constant constant : constants) {
			restrictToRange(constant);
			if (constant.isNegative() && constant.eContainer() instanceof Exponentiation) {
				constant.negate();
			}
		}
		
		// Done.
		return expression;
		
	}
	
	private void simplify(Element root) {
		boolean changed;
		do {
			changed = false;
			for (IotaRule rule : simplifyRules) {
				List<Element> matches = IotaRuleMatchFinder.findMatches(rule, root);
				changed = changed || !matches.isEmpty();
				for (Element match : matches) {
					rule.apply(match, true);
				}
			}
		} while (changed);
	}
	
	private void restrictToRange(Constant constant) {
		Constant lower = new Constant(lowerBound);
		Constant range = new Constant(upperBound - lowerBound);
		constant.mod(range);
		constant.add(lower);
	}
	
	public List<EClass> getExpressionTypes() {
		return expressionTypes;
	}

	public List<EquationType> getEquationTypes() {
		return equationTypes;
	}
	
	public List<IotaRule> getSimplifyRules() {
		return simplifyRules;
	}
	
	/**
	 * Initialize this generator with the default parameters.
	 */
	public void initWithDefaults() {
		
		equationTypes.clear();
		equationTypes.add(EquationType.EQUAL);
		equationTypes.add(EquationType.LESS);
		equationTypes.add(EquationType.GREATER);
		
		expressionTypes.clear();
		expressionTypes.add(IotaPackage.eINSTANCE.getConstant());
		expressionTypes.add(IotaPackage.eINSTANCE.getReference());
		expressionTypes.add(IotaPackage.eINSTANCE.getNegation());
		expressionTypes.add(IotaPackage.eINSTANCE.getAddition());
		expressionTypes.add(IotaPackage.eINSTANCE.getMultiplication());
		expressionTypes.add(IotaPackage.eINSTANCE.getDivision());
		expressionTypes.add(IotaPackage.eINSTANCE.getExponentiation());
		
	}
	
	private static <T> T randomElem(List<T> list) {
		int index = Math.min((int) (Math.random() * (double) list.size()), list.size()-1);
		return list.get(index);
	}
	
	private boolean containsOrIsExponentiation(Expression expression) {
		return !IotaFindUtil.getAllExpressions(expression,
			IotaPackage.eINSTANCE.getExponentiation()).isEmpty();
	}
}
